

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Nanjing Emergency Services Map</title>
  <link href="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.css" rel="stylesheet">
  <style>
    body { margin: 0; padding: 0; }

    /* Gray background overlay when the welcome panel is visible */
    .overlay {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.3);
      z-index: 2;
    }

    #map { position: absolute; top: 0; bottom: 0; width: 100%; }

    /* Welcome Panel Style */
    .welcome-panel {
      position: absolute;
      top: 50%;  /* Centering the panel vertically */
      left: 50%; /* Centering the panel horizontally */
      transform: translate(-50%, -50%);  /* Ensures the panel is exactly centered */
      background: rgba(255, 255, 255, 0.9);
      padding: 20px;
      border-radius: 10px;
      z-index: 3;
      width: 500px;  /* Fixed width to prevent it from becoming too wide */
      font-family: Arial, sans-serif;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);  /* Added subtle shadow */
      line-height: 1.5;
    }

    .welcome-panel h2 {
      margin: 0;
      font-size: 20px;
      font-weight: bold;
    }

    .welcome-panel p {
      font-size: 14px;
      margin: 10px 0;
    }

    .welcome-panel button {
      padding: 10px 20px;
      background-color: #FF5733;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }

    .welcome-panel button:hover {
      background-color: #C70039;
    }

    /* Center the author info and button */
    .author-info {
      margin-top: 20px;
      font-size: 12px;
      color: #555;
      text-align: center;  /* Center the content */
    }

    .author-info button {
      display: block;
      margin: 0 auto;  /* Center the button */
    }

    /* Controls */
    .controls {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(255, 255, 255, 0.8);
      padding: 15px;
      border-radius: 5px;
      z-index: 2;
      font-family: Arial, sans-serif;
      width: 300px;
    }

    .controls h4 {
      margin: 0;
      font-size: 16px;
      font-weight: bold;
    }

    .controls label {
      display: flex;
      align-items: center;
      margin: 5px 0;
    }

    .controls input[type="radio"] {
      margin-right: 10px;
    }

    .controls select, .controls button {
      width: 100%;
      margin-top: 5px;
    }

    /* Travel Mode horizontal layout */
    .controls .travel-mode {
      display: flex;
      justify-content: space-between;
    }

    /* Slider Styling */
    .controls input[type="range"] {
      width: 100%;
    }

    /* Info Panel Style */
    .info-panel {
      position: absolute;
      top: 10px;
      right: 10px;
      background: rgba(255, 255, 255, 0.9);
      padding: 15px;
      border-radius: 5px;
      z-index: 3;
      font-family: Arial, sans-serif;
      width: 250px;
    }

    .info-panel h5 {
      margin: 0;
      font-size: 18px;
      font-weight: bold;
    }

    .info-panel p {
      font-size: 14px;
      margin: 5px 0;
    }
  </style>
</head>
<body>
  <!-- Background Overlay (Gray) -->
  <div class="overlay" id="overlay"></div>

  <!-- Welcome Panel -->
  <div class="welcome-panel" id="welcomePanel">
    <h2>Welcome to Nanjing Emergency Services Map! 🏥🚒</h2>
    <p>This map helps analyze the service areas of major hospitals and fire stations in Nanjing. It aims to assess how quickly both fire stations and hospitals can be accessed, ensuring effective emergency response.</p>
    <p>The main features of this map include:</p>
    <ul>
      <li><strong>Service Areas:</strong> Mapping the coverage areas of hospitals and fire stations using the <strong>Voronoi algorithm</strong>.</li>
      <li><strong>Accessibility Zones:</strong> Calculating the areas that can be reached within different times using the <strong>Isochrone algorithm</strong>.</li>
    </ul>
    <p>This tool allows us to analyze the coordination between fire stations and hospitals in emergency situations, especially within critical time windows.</p>
    <div class="author-info">
      <button id="closeWelcomePanel">Close Panel</button>
      <p><em>Created by Xingye Zhang</em></p>
      <p>oliverxyzhang@gmail.com</p>
      <p>Fettes College</p>
    </div>
  </div>

  <div id="map"></div>
  <div class="controls">
    <h4>Select Service Layers</h4>
    <label><input type="checkbox" id="show-hospitals" checked><span style="background-color: #FF0000; width: 20px; height: 20px; display: inline-block;"></span> Hospitals</label>
    <label><input type="checkbox" id="show-firestations" checked><span style="background-color: #0000FF; width: 20px; height: 20px; display: inline-block;"></span> Fire Stations</label>
    <label><input type="checkbox" id="show-service-area-hospitals" checked><span style="background-color: #FF7F00; width: 20px; height: 20px; display: inline-block;"></span> Service Area for Hospitals</label>
    <label><input type="checkbox" id="show-service-area-firestations" checked><span style="background-color: #008000; width: 20px; height: 20px; display: inline-block;"></span> Service Area for Fire Stations</label>

    <h4>Accessibility Settings</h4>
    <div class="travel-mode">
      <label><input type="radio" name="travel-mode" value="walking" checked> Walking</label>
      <label><input type="radio" name="travel-mode" value="cycling"> Cycling</label>
      <label><input type="radio" name="travel-mode" value="driving"> Driving</label>
    </div>

    <label>Time:</label>
    <input type="range" id="isochrone-slider" min="5" max="60" value="10" step="5">
    <span id="time-value">10 minutes</span>

    <button id="get-accessibility">Get Accessibility Area</button>
  </div>

  <script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>
  <script>
    mapboxgl.accessToken = 'pk.eyJ1Ijoiemhhb2JvIiwiYSI6ImNqaW85NzNrdDA3OXczcHQ5aTZvYmtjc2gifQ.ummEQxsRIweCIdv9CRRzOw';
    const map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/streets-v12',
      center: [118.8062, 32.0603],
      zoom: 12
    });

    const hospitals = [
      { name: "南京国际医院", coordinates: [118.71392026387252, 32.09469721410999] },      
      { name: "南京市第二医院", coordinates: [118.76906995905694, 32.09502840459701] },
      { name: "南京市中西医结合医院", coordinates: [118.85764723084807, 32.03683947965712] },
      { name: "南京市建邺医院", coordinates: [118.72566667406106, 32.04255116843241] },  
      { name: "南京医院", coordinates: [118.82736850269444, 32.02655099679682] },   
      { name: "南京市市级机关医院", coordinates: [118.80240228611626, 32.06903460153056] },
      { name: "南京市中医院", coordinates: [118.80266212255117, 32.00475479365342] },
      { name: "江苏省人民医院", coordinates: [118.7390462263954, 32.06282815253032] },
      { name: "南京市医院医保定点医院", coordinates: [118.74002914501236, 32.06442361016104] },
      { name: "南京鼓楼医院", coordinates: [118.77882461676585, 32.06820587482283] },
      { name: "南京市鼓楼区中医院", coordinates: [118.76371841537511, 32.07780630565427] },
      { name: "南京玄武医院", coordinates: [118.79435915781094, 32.05210363886469] },
      { name: "南京市红十字医院", coordinates: [118.79031737352989, 32.02958927056289] },
      { name: "南京市第一医院", coordinates: [118.78314619217103, 32.0228686301513] },  
      { name: "南京儿童医院", coordinates: [118.7739688, 32.0525487] },
      { name: "中国人民解放军第八一医院", coordinates: [118.79237580142087, 32.041017308838825] },
      { name: "江苏省口腔医院", coordinates: [118.77486634128503, 32.049747609346426] },
      { name: "仙林医院", coordinates: [118.9035714, 32.0988245] },
      { name: "泰康仙林鼓楼医院", coordinates: [ 118.93617063884169, 32.096718568193616] },
      { name: "南京红山医院", coordinates: [118.8014697300523,  32.09894763792428] },
      { name: "南京朗盛医院", coordinates: [118.81857345438019, 32.142480584714896] },
      { name: "栖霞区医院", coordinates: [118.88115061981505, 32.11973337853644] },
      { name: "南京东瑞医院", coordinates: [118.8955855632974, 32.13550292902734] }
    ];

    const firestations = [
      { name: "南京市公安消防局特勤大队二中队", coordinates: [118.89375205925357, 32.13749743154813] },
      { name: "南京南站微型消防站", coordinates: [118.79794888318759, 31.96906631850641] },
      { name: "门东街区消防站", coordinates: [118.78665565153122, 32.016183645370205] },
      { name: "1912街区微型消防站", coordinates: [118.79588075421515, 32.04787763932942] },
      { name: "消防检验站", coordinates: [118.83083903739765, 32.09190172495607] },
      { name: "南京市黑龙江路消防站", coordinates: [118.77367889928283, 32.090466055978474] },
      { name: "南京金陵船厂消防队", coordinates: [118.75957510290134, 32.113582665502726] },
      { name: "南京消防集团", coordinates: [118.77193472235285, 32.044056701117356] },
      { name: "南京市鼓楼区公安消防大队汉中门中队", coordinates: [118.76661321959021, 32.042310599136165] },
      { name: "南京市建邺区公安消防大队", coordinates: [118.75991842579202, 32.02834058433972] },
      { name: "南京消防", coordinates: [118.73777410811044, 32.047257801980585] },
      { name: "南京扬子江隧道专职消防队执勤站", coordinates: [118.73202345189917, 32.06515280072154] },
      { name: "世茂微型消防站", coordinates: [118.73219511327862, 32.08013535053716] },
      { name: "南京市高新区公安消防大队", coordinates: [118.7084046, 32.166258] },
      { name: "Unnamed Fire Station", coordinates: [118.7961713, 32.042108] },
      { name: "金陵石化消防支队三大队", coordinates: [118.9157447, 32.145272] }
    ];

    const hospitalGeoJSON = {
      type: 'FeatureCollection',
      features: hospitals.map(hospital => ({
        type: 'Feature',
        properties: { name: hospital.name },
        geometry: { type: 'Point', coordinates: hospital.coordinates }
      }))
    };

    const firestationGeoJSON = {
      type: 'FeatureCollection',
      features: firestations.map(firestation => ({
        type: 'Feature',
        properties: { name: firestation.name },
        geometry: { type: 'Point', coordinates: firestation.coordinates }
      }))
    };

    map.on('load', () => {
      // Add hospital layer with red circles
      map.addSource('hospitals', { type: 'geojson', data: hospitalGeoJSON });
      map.addLayer({
        id: 'hospitals-layer',
        type: 'circle',
        source: 'hospitals',
        paint: {
            'circle-color': '#FF0000',
            'circle-radius': 8,
            'circle-blur': 0.1,
            'circle-stroke-color': 'rgba(255,255,255,0.8)',
            'circle-stroke-width': 3
        }
      });

      // Add fire station layer with blue circles
      map.addSource('firestations', { type: 'geojson', data: firestationGeoJSON });
      map.addLayer({
        id: 'firestations-layer',
        type: 'circle',
        source: 'firestations',
        paint: {
            'circle-color': '#0000FF',
            'circle-radius': 8,
            'circle-blur': 0.1,
            'circle-stroke-color': 'rgba(255,255,255,0.8)',
            'circle-stroke-width': 3
        }
      });

      // Generate Voronoi (Service Area) for hospitals and fire stations
      generateServiceArea(hospitals, 'service-area-hospitals', '#FF7F00');
      generateServiceArea(firestations, 'service-area-firestations', '#008000');
    });

    function generateServiceArea(points, layerId, color) {
      const pointCollection = turf.featureCollection(
        points.map(p => turf.point(p.coordinates))
      );
      const voronoiPolygons = turf.voronoi(pointCollection, {
        bbox: [
        118.66373215308204,
        31.93731579464535,  
        118.96653778119298, 
        32.18017044467648   
        ]
    });




      map.addSource(layerId, {
        type: 'geojson',
        data: voronoiPolygons
      });

      map.addLayer({
        id: layerId,
        type: 'line',
        source: layerId,
        paint: {
          'line-color': color,
          'line-width': 3
        }
      }, 'hospitals-layer');
    }

    // Update slider value display
    document.getElementById('isochrone-slider').addEventListener('input', function() {
      document.getElementById('time-value').textContent = this.value + ' minutes';
      updateAccessibilityArea();
    });

    // Update accessibility when travel mode or slider changes
    document.querySelectorAll('input[name="travel-mode"]').forEach((radio) => {
      radio.addEventListener('change', updateAccessibilityArea);
    });

    // Show/hide layers based on checkbox
    document.getElementById('show-hospitals').addEventListener('change', (e) => {
      map.setLayoutProperty('hospitals-layer', 'visibility', e.target.checked ? 'visible' : 'none');
    });

    document.getElementById('show-firestations').addEventListener('change', (e) => {
      map.setLayoutProperty('firestations-layer', 'visibility', e.target.checked ? 'visible' : 'none');
    });

    document.getElementById('show-service-area-hospitals').addEventListener('change', (e) => {
      map.setLayoutProperty('service-area-hospitals', 'visibility', e.target.checked ? 'visible' : 'none');
    });

    document.getElementById('show-service-area-firestations').addEventListener('change', (e) => {
      map.setLayoutProperty('service-area-firestations', 'visibility', e.target.checked ? 'visible' : 'none');
    });

    // Close Welcome Panel on button click
    document.getElementById('closeWelcomePanel').addEventListener('click', () => {
      document.getElementById('welcomePanel').style.display = 'none';
      document.getElementById('overlay').style.display = 'none';
    });

    let marker = null;

    map.on('click', async (e) => {
      const coordinates = e.lngLat;

      if (marker) {
        marker.remove();
      }

      // Create the dynamic pulsing marker
      marker = new mapboxgl.Marker({ element: createPulsingDot() })
        .setLngLat(coordinates)
        .addTo(map);

      await updateAccessibilityArea();  // Update accessibility when marker is placed
    });

// 复用一个 Popup，避免重复创建
const infoPopup = new mapboxgl.Popup({
  closeButton: true,
  closeOnClick: true
});

// 鼠标样式 - 进入可点图层时显示手型
map.on('mouseenter', 'hospitals-layer', () => {
  map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseenter', 'firestations-layer', () => {
  map.getCanvas().style.cursor = 'pointer';
});

// 点击医院点，显示名称
map.on('click', 'hospitals-layer', (e) => {
  const feature = e.features && e.features[0];
  if (!feature) return;

  // 处理跨经度复制时坐标偏移问题
  const coordinates = feature.geometry.coordinates.slice();
  const name = feature.properties.name;

  while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
  }

  infoPopup
    .setLngLat(coordinates)
    .setHTML(`<strong>${name}</strong>`)
    .addTo(map);
});

// 点击消防站点，显示名称
map.on('click', 'firestations-layer', (e) => {
  const feature = e.features && e.features[0];
  if (!feature) return;

  const coordinates = feature.geometry.coordinates.slice();
  const name = feature.properties.name;

  while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
  }

  infoPopup
    .setLngLat(coordinates)
    .setHTML(`<strong>${name}</strong>`)
    .addTo(map);
});


    map.on('mouseleave', 'hospitals-layer', () => {
      map.getCanvas().style.cursor = '';
    });

    map.on('mouseleave', 'firestations-layer', () => {
      map.getCanvas().style.cursor = '';
    });

    // Create pulsing dot as a custom marker element
    function createPulsingDot() {
      const size = 50;  // Size of the pulsing dot
      const canvas = document.createElement('canvas');
      canvas.width = size;
      canvas.height = size;
      const context = canvas.getContext('2d');

      let t = 0; // Animation time
      function render() {
        t += 0.02; // Increase time
        const radius = (size / 2) * 0.3;
        const outerRadius = (size / 2) * 0.7 * Math.abs(Math.sin(t)) + radius;

        context.clearRect(0, 0, size, size);
        context.beginPath();
        context.arc(size / 2, size / 2, outerRadius, 0, Math.PI * 2);
        context.fillStyle = `rgba(255, 200, 200, ${1 - Math.abs(Math.sin(t))})`;
        context.fill();

        context.beginPath();
        context.arc(size / 2, size / 2, radius, 0, Math.PI * 2);
        context.fillStyle = 'rgba(255, 100, 100, 1)';
        context.strokeStyle = 'white';
        context.lineWidth = 2 + 4 * (1 - Math.abs(Math.sin(t)));
        context.fill();
        context.stroke();

        // Repainting the canvas
        requestAnimationFrame(render);
      }
      render();

      return canvas;
    }

    // Update accessibility area (replaces isochrone)
    async function updateAccessibilityArea() {
      const travelMode = document.querySelector('input[name="travel-mode"]:checked').value;
      const time = document.getElementById('isochrone-slider').value;

      let coordinates;
      if (marker) {
        coordinates = marker.getLngLat();
      } else {
        coordinates = map.getCenter();  // Default to map center if no marker
      }

      const url = `https://api.mapbox.com/isochrone/v1/mapbox/${travelMode}/${coordinates.lng},${coordinates.lat}?contours_minutes=${time}&polygons=true&access_token=${mapboxgl.accessToken}`;

      const response = await fetch(url);
      const data = await response.json();

      if (data && data.features) {
        const accessibilityLayer = {
          id: 'accessibility-layer',
          type: 'fill',
          source: {
            type: 'geojson',
            data: data
          },
          paint: {
            'fill-color': '#3bb2d0',
            'fill-opacity': 0.5
          }
        };

        // Remove existing accessibility layer and add the new one
        if (map.getLayer('accessibility-layer')) {
          map.removeLayer('accessibility-layer');
          map.removeSource('accessibility-layer');
        }

        map.addLayer(accessibilityLayer, 'hospitals-layer');
      }
    }
  </script>
</body>
</html>
